import csharp

/**
 * A package reference in an XML file, for example in a
 * `.csproj` file, a `.props` file, or a `packages.config` file.
 */
class Package extends XMLElement {
  string name;
  Version version;

  Package() {
    (this.getName() = "PackageManagement" or this.getName() = "PackageReference") and
    name = this.getAttributeValue("Include") and
    version = this.getAttributeValue("Version")
    or
    this.getName() = "package" and
    name = this.getAttributeValue("id") and
    version = this.getAttributeValue("version")
  }

  /** Gets the name of the package, for example `System.IO.Pipelines`. */
  string getPackageName() { result = name }

  /** Gets the version of the package, for example `4.5.1`. */
  Version getVersion() { result = version }

  override string toString() { result = name + " " + version }
}

/**
 * A vulnerability, where the name of the vulnerability is this string.
 * One of `matchesRange` or `matchesVersion` must be overridden in order to
 * specify which packages are vulnerable.
 */
abstract class Vulnerability extends string {
  bindingset[this]
  Vulnerability() { any() }

  /**
   * Holds if a package with name `name` is vulnerable from version `affected`
   * until version `fixed`.
   */
  predicate matchesRange(string name, Version affected, Version fixed) { none() }

  /**
   * Holds if a package with name `name` is vulnerable in version `affected`, and
   * is fixed by version `fixed`.
   */
  predicate matchesVersion(string name, Version affected, Version fixed) { none() }

  /** Gets the URL describing the vulnerability. */
  abstract string getUrl();

  /**
   * Holds if a package with name `name` and version `version`
   * has this vulnerability. The fixed version is given by `fixed`.
   */
  bindingset[name, version]
  predicate isVulnerable(string name, Version version, Version fixed) {
    exists(Version affected, string n | name.toLowerCase() = n.toLowerCase() |
      matchesRange(n, affected, fixed) and
      version.compareTo(fixed) < 0 and
      version.compareTo(affected) >= 0
      or
      matchesVersion(n, affected, fixed) and
      version.compareTo(affected) = 0
    )
  }
}

bindingset[name, version]
private Version getUltimateFix(string name, Version version) {
  result = max(Version fix | any(Vulnerability v).isVulnerable(name, version, fix))
}

/**
 * A package with a vulnerability.
 */
class VulnerablePackage extends Package {
  Vulnerability vuln;

  VulnerablePackage() { vuln.isVulnerable(this.getPackageName(), this.getVersion(), _) }

  /** Gets the vulnerability of this package. */
  Vulnerability getVulnerability() { result = vuln }

  /** Gets the version of this package where the vulnerability is fixed. */
  Version getFixedVersion() {
    // This is needed because sometimes the "fixed" version of some
    // vulnerabilities are themselves vulnerable to other vulnerabilities.
    result = getUltimateFix(this.getPackageName(), this.getVersion())
  }
}
